/*
 * #%L
 * Alfresco Remote API
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.web.scripts.calendar;

import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;

import org.alfresco.query.PagingRequest;
import org.alfresco.repo.calendar.CalendarModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.activities.ActivityService;
import org.alfresco.service.cmr.calendar.CalendarEntry;
import org.alfresco.service.cmr.calendar.CalendarService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
import org.alfresco.util.ISO8601DateFormat;
import org.alfresco.util.ScriptPagingDetails;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.json.JSONException;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.DeclarativeWebScript;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.json.JSONWriter;

/**
 * @author Nick Burch
 * @since 4.0
 */
public abstract class AbstractCalendarWebScript extends DeclarativeWebScript
{
    private static final String ALL_DAY_DATETIME_PATTERN = "yyyy-MM-dd'T00:00:00.000'";

    private static final DateTimeFormatter ALL_DAY_DATETIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T00:00:00.000'");

    public static final String CALENDAR_SERVICE_ACTIVITY_APP_NAME = "calendar";
    
    protected static final String MSG_EVENT_NOT_FOUND = "calendar.err.event.not.found";
    protected static final String MSG_INVALID_JSON = "calendar.err.invalid.json";
    
    protected static final String PARAM_TIMEZONE = "timeZone";
    protected static final String PARAM_START_AT = "startAt";
    protected static final String PARAM_END_AT   = "endAt";
    protected static final String PARAM_ISO8601  = "iso8601";
    
    private static Log logger = LogFactory.getLog(AbstractCalendarWebScript.class);
    
    /**
     * When no maximum or paging info is given, what should we use?
     */
    protected static final int MAX_QUERY_ENTRY_COUNT = 1000;
   
    // Injected services
    protected NodeService nodeService;
    protected SiteService siteService;
    protected ActivityService activityService;
    protected CalendarService calendarService;
    
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    
    public void setSiteService(SiteService siteService)
    {
        this.siteService = siteService;
    }
    
    public void setActivityService(ActivityService activityService)
    {
        this.activityService = activityService;
    }
    
    public void setCalendarService(CalendarService calendarService)
    {
        this.calendarService = calendarService;
    }
    
    /**
     * Gets the date from the String, trying the various formats
     *  (New and Legacy) until one works...
     */
    protected Date parseDate(String date)
    {
       // Is there one at all?
       if (date == null || date.length() == 0)
       {
          return null;
       }
       
       // Today's Date - special case 
       if (date.equalsIgnoreCase("NOW"))
       {
           // We want all of today, so go back to midnight
           Calendar c = Calendar.getInstance();
           c.set(Calendar.HOUR_OF_DAY, 0);
           c.set(Calendar.MINUTE, 0);
           c.set(Calendar.SECOND, 0);
           c.set(Calendar.MILLISECOND, 0);
           return c.getTime();
       }
       
       // Try as ISO8601
       try
       {
          return ISO8601DateFormat.parse(date);
       }
       catch (Exception e) {}
       
       // Try YYYY/MM/DD
       SimpleDateFormat slashtime = new SimpleDateFormat("yyyy/MM/dd HH:mm");
       SimpleDateFormat slash = new SimpleDateFormat("yyyy/MM/dd");
       try
       {
          return slashtime.parse(date);
       }
       catch (ParseException e) {}
       try
       {
          return slash.parse(date);
       }
       catch (ParseException e) {}
       
       // Try YYYY-MM-DD
       SimpleDateFormat dashtime = new SimpleDateFormat("yyyy-MM-dd HH:mm");
       SimpleDateFormat dash = new SimpleDateFormat("yyyy-MM-dd");
       try
       {
          return dashtime.parse(date);
       }
       catch (ParseException e) {}
       try
       {
          return dash.parse(date);
       }
       catch (ParseException e) {}

       // We don't know what it is, object
       throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid date '" + date + "'");
    }
    
    /**
     * Extracts the Start and End details, along with the All Day flag
     *  from the JSON, and returns if the event is all day or not
     */
    protected boolean extractDates(CalendarEntry entry, JSONObject json) throws JSONException
    {
       boolean isAllDay = false;
       if (json.containsKey("allday"))
       {
          isAllDay = true;
       }
       
       if (json.containsKey(PARAM_START_AT) && json.containsKey(PARAM_END_AT))
       {
          // New style ISO8601 based dates and times
          Object startAtO = json.get(PARAM_START_AT);
          Object endAtO = json.get(PARAM_END_AT);
          
          // Grab the details
          String startAt;
          String endAt;
          String timezoneName = null;
          if(startAtO instanceof JSONObject)
          {
             // "startAt": { "iso8601":"2011-...." }
             JSONObject startAtJSON = (JSONObject)startAtO; 
             JSONObject endAtJSON = (JSONObject)endAtO; 
             startAt = (String)startAtJSON.get(PARAM_ISO8601); 
             endAt = (String)endAtJSON.get(PARAM_ISO8601);
             
             if(startAtJSON.containsKey(PARAM_TIMEZONE))
             {
                timezoneName = (String)startAtJSON.get(PARAM_TIMEZONE);
                if(endAtJSON.containsKey(PARAM_TIMEZONE))
                {
                   String endTZ = (String)endAtJSON.get(PARAM_TIMEZONE);
                   if(! endTZ.equals(timezoneName))
                   {
                      throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Timezones must match");
                   }
                }
             }
          }
          else
          {
             // "startAt": "2011-...."
             startAt = (String)json.get(PARAM_START_AT);
             endAt = (String)json.get(PARAM_END_AT);
          }
          if(json.containsKey(PARAM_TIMEZONE))
          {
             timezoneName = (String)json.get(PARAM_TIMEZONE);
          }
          
          
          // Is this an all day event?
          if (json.containsKey("allday"))
          {
             // Store it as UTC midnight to midnight
             // Reset the time part to ensure that
             String utcMidnight = "T00:00:00Z";
             startAt = startAt.substring(0, 10) + utcMidnight;
             endAt = endAt.substring(0, 10) + utcMidnight;
             entry.setStart(ISO8601DateFormat.parse(startAt));
             entry.setEnd(ISO8601DateFormat.parse(endAt));
          }
          else
          {
             // Regular event start and end rules
             
             // Do we have explicit timezone information?
             if (timezoneName != null)
             {
                // Get the specified timezone
                TimeZone tz = TimeZone.getTimeZone(timezoneName);
                
                // Grab the dates and times in the specified timezone
                entry.setStart(ISO8601DateFormat.parse(startAt, tz));
                entry.setEnd(ISO8601DateFormat.parse(endAt, tz));
             }
             else
             {
                // Offset info is either in the date, or we just have to guess
                entry.setStart(parseDate(startAt));
                entry.setEnd(parseDate(endAt));
             }
          }
       }
       else if (json.containsKey("allday"))
       {
          // Old style all-day event
          Date start = parseDate(getOrNull(json, "from"));
          Date end = parseDate(getOrNull(json, "to"));
          // Store it as UTC midnight to midnight
          // Reset the time part to ensure that
          String isoStartAt = ISO8601DateFormat.format(start);
          String isoEndAt =  ISO8601DateFormat.format(end);
    	  String utcMidnight = "T00:00:00Z";
          isoStartAt = isoStartAt.substring(0, 10) + utcMidnight;
          isoEndAt = isoEndAt.substring(0, 10) + utcMidnight;
          entry.setStart(ISO8601DateFormat.parse(isoStartAt));
          entry.setEnd(ISO8601DateFormat.parse(isoEndAt));
    	  
    	 
       }
       else
       {
          // Old style regular event
          entry.setStart(parseDate((String)json.get("from") + " " + (String)json.get("start")));
          entry.setEnd(parseDate((String)json.get("to") + " " + (String)json.get("end")));
       }
       
       return isAllDay;
    }
    
    protected String getOrNull(JSONObject json, String key) throws JSONException
    {
       if (json.containsKey(key))
       {
          return (String)json.get(key);
       }
       return null;
    }
    
    /**
     * Builds up a listing Paging request, either using the defaults or
     *  the paging options specified
     */
    protected PagingRequest buildPagingRequest(WebScriptRequest req)
    {
       return new ScriptPagingDetails(req, MAX_QUERY_ENTRY_COUNT);
    }
    
    /**
     * Normally the Calendar webscripts return a 200 with JSON
     *  containing the error message. Override this to switch to
     *  using HTTP status codes instead
     */
    protected boolean useJSONErrors()
    {
       return true;
    }
    
    /**
     * Equivalent of <i>jsonError</i> in the old JavaScript controllers
     */
    protected Map<String,Object> buildError(String message)
    {
       HashMap<String, Object> result = new HashMap<String, Object>();
       result.put("error", message);
       
       HashMap<String, Object> model = new HashMap<String, Object>();
       model.put("error", message);
       model.put("result", result);
       
       return model;
    }

    /**
     * Generates an activity entry for the entry
     */
    protected String addActivityEntry(String event, CalendarEntry entry, SiteInfo site, 
          WebScriptRequest req, JSONObject json)
    {
       SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
       String dateOpt = "?date=" + fmt.format(entry.getStart());
       
       // What page is this for?
       String page = req.getParameter("page");
       if (page == null && json != null)
       {
          if (json.containsKey("page"))
          {
             page = (String)json.get("page");
          }
       }
       if (page == null)
       {
          // Default
          page = "calendar";
       }
       
       try
       {
          StringWriter activityJson = new StringWriter();
          JSONWriter activity = new JSONWriter(activityJson);
          activity.startObject();
          activity.writeValue("title", entry.getTitle());
          activity.writeValue("page", page + dateOpt);
          activity.endObject();
          
          activityService.postActivity(
                "org.alfresco.calendar.event-" + event,
                site.getShortName(),
                CALENDAR_SERVICE_ACTIVITY_APP_NAME,
                activityJson.toString());
       }
       catch (Exception e)
       {
          // Warn, but carry on
          logger.warn("Error adding event " + event + " to activities feed", e);
       }
       
       // Return the date we used
       return dateOpt;
    }
    
    /**
     * For an event that is a recurring event, have an ignored child event
     *  generated for it
     */
    protected NodeRef createIgnoreEvent(WebScriptRequest req, CalendarEntry parent)
    {
       // Get the date to be ignored
       Map<QName,Serializable> props = new HashMap<QName, Serializable>();
       Date date = parseDate(req.getParameter("date"));
       props.put(CalendarModel.PROP_IGNORE_EVENT_DATE, date);
       
       // Create a child node of the event
       NodeRef ignored = nodeService.createNode(
             parent.getNodeRef(), CalendarModel.ASSOC_IGNORE_EVENT_LIST,
             QName.createQName(GUID.generate()), CalendarModel.TYPE_IGNORE_EVENT, props
       ).getChildRef();
       
       // No further setup is needed
       return ignored;
    }
    
    @Override
    protected Map<String, Object> executeImpl(WebScriptRequest req,
          Status status, Cache cache) 
    {
       Map<String, String> templateVars = req.getServiceMatch().getTemplateVars();
       if (templateVars == null)
       {
          String error = "No parameters supplied";
          if (useJSONErrors())
          {
             return buildError(error);
          }
          else
          {
             throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
          }
       }
       
       
       // Parse the JSON, if supplied
       JSONObject json = null;
       String contentType = req.getContentType();
       if (contentType != null && contentType.indexOf(';') != -1)
       {
          contentType = contentType.substring(0, contentType.indexOf(';'));
       }
       if (MimetypeMap.MIMETYPE_JSON.equals(contentType))
       {
          JSONParser parser = new JSONParser();
          try
          {
             json = (JSONObject)parser.parse(req.getContent().getContent());
          }
          catch (IOException io)
          {
             return buildError("Invalid JSON: " + io.getMessage());
          }
          catch (org.json.simple.parser.ParseException je)
          {
             return buildError("Invalid JSON: " + je.getMessage());
          }
       }
       
       
       // Get the site short name. Try quite hard to do so...
       String siteName = templateVars.get("siteid");
       if (siteName == null)
       {
          siteName = templateVars.get("site");
       }
       if (siteName == null)
       {
          siteName = req.getParameter("site");
       }
       if (siteName == null && json != null)
       {
          if (json.containsKey("siteid"))
          {
             siteName = (String)json.get("siteid");
          }
          else if (json.containsKey("site"))
          {
             siteName = (String)json.get("site");
          }
       }
       if (siteName == null)
       {
          String error = "No site given";
          if (useJSONErrors())
          {
             return buildError("No site given");
          }
          else
          {
             throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
          }
       }
       
       // Grab the requested site
       SiteInfo site = siteService.getSite(siteName);
       if (site == null)
       {
          String error = "Could not find site: " + siteName;
          if (useJSONErrors())
          {
             return buildError(error);
          }
          else
          {
             throw new WebScriptException(Status.STATUS_NOT_FOUND, error);
          }
       }
       
       // Event name is optional
       String eventName = templateVars.get("eventname");
       
       // Have the real work done
       return executeImpl(site, eventName, req, json, status, cache); 
    }
    
       
    /**
     * Removes the time zone for a given date if the Calendar Entry is an all day event
     * 
     * @return ISO 8601 formatted date String
     */
    protected String removeTimeZoneIfRequired(Date date, Boolean isAllDay, Boolean removeTimezone)
    {
        return removeTimeZoneIfRequired(date, isAllDay, removeTimezone, null);
    }

    /**
     * Removes the time zone for a given date if the Calendar Entry is an all day event
     * 
     * @return ISO 8601 formatted date String if datePattern is null
     */
    protected String removeTimeZoneIfRequired(Date date, Boolean isAllDay, Boolean removeTimezone, String datePattern)
    {
        if (removeTimezone)
        {
            DateTime dateTime = new DateTime(date, DateTimeZone.UTC);

            if (null == datePattern)
            {
                return dateTime.toString((isAllDay) ? (ALL_DAY_DATETIME_FORMATTER) : (ISODateTimeFormat.dateTime()));
            }
            else
            {
                // For Legacy Dates and Times.
                return dateTime.toString(DateTimeFormat.forPattern(datePattern));
            }
        }

        // This is for all other cases, including the case, when UTC time zone is configured

        if (!isAllDay && (null == datePattern))
        {
            return ISO8601DateFormat.format(date);
        }

        DateFormat simpleDateFormat = new SimpleDateFormat((null != datePattern) ? (datePattern) : (ALL_DAY_DATETIME_PATTERN));

        return simpleDateFormat.format(date);
    }

    protected abstract Map<String, Object> executeImpl(SiteInfo site, 
          String eventName, WebScriptRequest req, JSONObject json, 
          Status status, Cache cache);
}
